from SchemaLogger import *
from SchemaConst import *
from SchemaType import *

import re

def CheckSchema(key, value, schema):
    return _check(key, value, schema, value)

# SchemaType could be combined by bit flags
# It will separate the bit flags and return the collection
def _getBitFlags(value):
    flags = []
    power = 0
    while value > 0:
        flag = value % 2
        value = int(value / 2)
        if flag == 0x01:
            flags.append(flag << power)
        power += 1
    return flags

# The (key, value) could hide deep in the config tree
# It will check the tree recursively and return the result if there is
def _getKeyValueInConfig(key, config):
    isDict = isinstance(config, dict)
    isList = isinstance(config, list)
    resultValue = None
    if not isDict and not isList:
        return resultValue
    if isDict:
        if key in config:
            resultValue = config[key]
            return resultValue
        for k in config:
            resultValue = _getKeyValueInConfig(key, config[k])
            if resultValue is not None:
                return resultValue
    else:
        for v in config:
            resultValue = _getKeyValueInConfig(key, v)
            if resultValue is not None:
                return resultValue
    return resultValue

def _checkType(key, value, keySchema):
    for typeFlag in _getBitFlags(keySchema[SchemaType]):
        if typeFlag not in MetaType:
            Error("Schema type is not defined: " + str(keySchema[SchemaType]) + ", invalid flag: " + str(typeFlag))
            return False
        if isinstance(value, MetaType[typeFlag][0]):
            return True
    Error("Config type and value is not matched: " + key)
    return False

def _checkRegKey(key, schema):
    for k in schema:
        if re.fullmatch(k, key):
            return k
    return None

def _check(key, value, schema, config, recursiveChain = []):
    if key not in schema:
        # First use regext to check if key is matched
        regKey = _checkRegKey(key, schema)
        if regKey:
            key = regKey
        # Then check if there is key wild card defined
        elif SchemaAnyOther in schema:
            key = SchemaAnyOther
        else:
            Error("Key does not defined in schema: " + key)
            return False
    keySchema = _cloneSchema(schema[key], schema, [])
    if keySchema is None:
        Error("Could not get schema for key: " + key)
        return False
    return _doCheck(key, value, keySchema, schema, config, recursiveChain)

def _doCheck(key, value, keySchema, schema, config, recursiveChain = []):
    if not _checkType(key, value, keySchema):
        return False
    isCheckChild = True
    if SchemaRule in keySchema:
        for rule in keySchema[SchemaRule]:
            rule.recursiveChain = _cloneRecursiveChain(recursiveChain)
            if not rule.Check(key, value, schema, config):
                return False
            if isinstance(rule, IgnoreChildSchema):
                isCheckChild = False
    if isCheckChild and isinstance(value, dict):
        for k in value:
            recursiveChainClone = _cloneRecursiveChain(recursiveChain)
            if not _check(k, value[k], schema, config, recursiveChainClone):
                return False
    return True

def _cloneSchema(schemaToClone, schema, recursiveChain):
    result = {}
    for key in schemaToClone:
        if key != SchemaInherit:
            result[key] = schemaToClone[key]
    if SchemaInherit in schemaToClone:
        inheritKey = schemaToClone[SchemaInherit]
        if inheritKey in recursiveChain:
            Warning("Inherit key recursive: " + inheritKey)
            return result
        if inheritKey not in schema:
            Error("Inherit key is not defined in schema: " + inheritKey)
            return None
        recursiveChain.append(inheritKey)
        inheritSchema = _cloneSchema(schema[inheritKey], schema, recursiveChain)
        if inheritSchema is None:
            return None
        _mergeSchema(result, inheritSchema)
    return result

def _cloneRecursiveChain(recursiveChain):
    return recursiveChain.copy()

def _mergeSchema(schemaTo, schemaFrom):
    for key in schemaFrom:
        if key in schemaTo:
            # Only merge dict
            if isinstance(schemaTo[key], dict) and isinstance(schemaFrom[key], dict):
                _mergeSchema(schemaTo[key], schemaFrom[key])
        else:
            schemaTo[key] = schemaFrom[key]

class BaseRule:
    def __init__(self, recursiveChain = []):
        self.__recursiveChain = recursiveChain
    def Check(self, key, value, schema, config):
        return False
    @property
    def recursiveChain(self):
        return self.__recursiveChain
    @recursiveChain.setter
    def recursiveChain(self, recursiveChain):
        self.__recursiveChain = recursiveChain

# For dict value, all keys should be defined in given collection list
class KeyIn(BaseRule):
    def __init__(self, collection):
        self.__collection = collection
    def Check(self, key, value, schema, config):
        if not isinstance(value, dict):
            Error("It's not dict type of value on check KeyIn")
            return False
        if not isinstance(self.__collection, list):
            Error("It's not list type on check KeyIn: " + self.__collection)
            return False
        for k in value:
            if k not in self.__collection:
                Error("Key is not in specified collection: " + k)
                return False
        return True

# The value should be in given collection list
class ValueIn(BaseRule):
    def __init__(self, collection):
        self.__collection = collection
    def Check(self, key, value, schema, config):
        if not isinstance(self.__collection, list):
            Error("It's not list type on check ValueIn: " + self.__collection)
            return False
        for v in self.__collection:
            if value == v:
                return True
        Error("Value is not in specified collection: " + value)
        return False

# This is used to check the bit flag combination value are all contained in collection
class EachBitFlagValueIn(BaseRule):
    def __init__(self, collection):
        self.__collection = collection
    def Check(self, key, value, schema, config):
        if not isinstance(self.__collection, list):
            Error("It's not list type on check EachBitFlagValueIn: " + self.__collection)
            return False
        for flag in _getBitFlags(value):
            if flag not in self.__collection:
                Error("Bit flag is not in specified collection: " + flag)
                return False
        return True

# For dict value, it should have all the keys in passed parameters
class HasKey(BaseRule):
    def __init__(self, *args):
        self.__keys = args
    def Check(self, key, value, schema, config):
        if not isinstance(value, dict):
            Error("It's not dict type of value on check HasKey")
            return False
        for k in self.__keys:
            if k not in value:
                Error("Key is not defined: " + k + ", " + key)
                return False
        return True

# For dict value, it should have at least one key in passed parameters
class AtLeastOneKey(BaseRule):
    def __init__(self, *args):
        self.__keys = args
    def Check(self, key, value, schema, config):
        if not isinstance(value, dict):
            Error("It's not dict type of value on check AtLeastOneKey")
            return False
        for k in value:
            if k in self.__keys:
                return True
        Error("None of required key is defined: " + key)
        return False

# Redirect the schema check to another key
# The new key is defined by "schemaKey" parameter
class CheckAsType(BaseRule):
    def __init__(self, schemaKey):
        self.__schemaKey = schemaKey
    def Check(self, key, value, schema, config):
        if self.__schemaKey not in schema:
            Error("Schema key is not defined in schema when check for type: " + self.__schemaKey)
            return False
        keySchema = _cloneSchema(schema[self.__schemaKey], schema, [])
        if keySchema is None:
            return False
        return _doCheck(self.__schemaKey, value, keySchema, schema, config, self.recursiveChain)

# Redirect the schema check to another key
# And the new key is defined by value of one key in dict (config)
class CheckAsTypeFromKey(BaseRule):
    def __init__(self, key):
        self.__key = key
    def Check(self, key, value, schema, config):
        if not isinstance(value, dict):
            Error("It's not dict type of value on check CheckAsTypeFromKey")
            return False
        if self.__key not in value:
            Error("Key of type is not defined: " + self.__key + ", " + key)
            return False
        typeFromKey = value[self.__key]
        if typeFromKey not in schema:
            Error("Type is not defined in schema: " + typeFromKey)
            return False
        recursiveChain = self.recursiveChain
        recursiveKey = "CheckAsTypeFromKey." + typeFromKey
        if recursiveKey in recursiveChain:
            Debug("Check type from key recursive: " + typeFromKey)
            # This rule may be set to both parent schema and child schema
            # When parent schema is checking, this rule will redirect to check schema (child schema) defined by key value in config
            # And the key is checked by child schema, which may inherit parent schema
            # So this rule which is derived from parent will be checked again recursively
            # Just return True under this situation to break the recursive but do not break the schema check
            return True
        keySchema = _cloneSchema(schema[typeFromKey], schema, [])
        if keySchema is None:
            return False
        recursiveChain.append(recursiveKey)
        return _doCheck(typeFromKey, value, keySchema, schema, config, self.recursiveChain)

# Check value is not empty
# The value type is defined in SchemaConst
class NotEmpty(BaseRule):
    def __init__(self, dataType):
        self.__dataType = dataType
    def Check(self, key, value, schema, config):
        if self.__dataType not in MetaType:
            Error("Schema data type is not defined: " + self.__dataType)
            return False
        if MetaType[self.__dataType][1](value):
            Error("Value is empty: " + key)
            return False
        return True

# For list value, check each item as given key
class CheckForeachAsType(BaseRule):
    def __init__(self, schemaKey):
        self.__schemaKey = schemaKey
    def Check(self, key, value, schema, config):
        isList = isinstance(value, list)
        isDict = isinstance(value, dict)
        if not isList and not isDict:
            Error("It's not list or dict type on check CheckForeachAsType: " + value)
            return False
        if self.__schemaKey not in schema:
            Error("Schema key is not defined in schema when check for each: " + self.__schemaKey)
            return False
        keySchema = _cloneSchema(schema[self.__schemaKey], schema, [])
        if keySchema is None:
            return False
        if isList:
            for v in value:
                if not _doCheck(self.__schemaKey, v, keySchema, schema, config, self.recursiveChain):
                    return False
        else:
            for k in value:
                if not _doCheck(self.__schemaKey, value[k], keySchema, schema, config, self.recursiveChain):
                    return False
        return True

# For list or dict value, check each item as given key recursively
# If one element in list or dict is id (string), it will be taken as key
# And will recursive to the value of the key to check real value
class CheckForeachAsTypeRecursively(BaseRule):
    def __init__(self, schemaKey):
        self.__schemaKey = schemaKey
    def Check(self, key, value, schema, config):
        isList = isinstance(value, list)
        isDict = isinstance(value, dict)
        if not isList and not isDict:
            Error("It's not list for dict type on check CheckForeachAsTypeRecursively: " + value)
            return False
        if self.__schemaKey not in schema:
            Error("Schema key is not defined in schema when check for each recursively: " + self.__schemaKey)
            return False
        keySchema = _cloneSchema(schema[self.__schemaKey], schema, [])
        if keySchema is None:
            return False
        if isList:
            for v in value:
                if not self.__checkEachValue(v, keySchema, schema, config):
                    return False
        else:
            for k in value:
                if not self.__checkEachValue(value[k], keySchema, schema, config):
                    return False
        return True
    def __checkEachValue(self, value, keySchema, schema, config):
        if isinstance(value, str):
            keyValue = _getKeyValueInConfig(value, config)
            if keyValue is not None:
                # Check recursively
                recursiveChain = self.recursiveChain
                recursiveKey = "CheckForeachAsTypeRecursively." + value
                if recursiveKey not in recursiveChain:
                    recursiveChain.append(recursiveKey)
                    if not _doCheck(self.__schemaKey, keyValue, keySchema, schema, config, recursiveChain):
                        return False
                else:
                    Error("Check foreach recursive: " + value)
                    return False
            else:
                Warning("The key is not defined in config: " + key)
                return False
        else:
            # Check normal value
            if not _doCheck(self.__schemaKey, value, keySchema, schema, config, self.recursiveChain):
                return False
        return True

# By default, schema will check each key in dict value
# This rule will skip this check
class IgnoreChildSchema(BaseRule):
    def __inti__(self):
        pass
    def Check(self, key, value, schema, config):
        return True

# The value of inherit key should be defined in schema definition
# It is used in schema file check not user config file check
# It is used to check inherited key are also defined in schema file
class KeyValueInSchema(BaseRule):
    def __init__(self):
        pass
    def Check(self, key, value, schema, config):
        return True if key in schema else False
